/* MusicNet Alignment Tool
 *
 * Copyright (c) 2010 Joe Lambert
 * 
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 * 
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *  
 */

/** section: alignment
 * class MatchedObject
 *  
 **/

// A single item
var MatchedObject = Class.create({
	initialize: function(label, id)
	{
		this.label = label;
		this.id = id;
	},

/**
 * MatchedObject#toString() -> String
 * 
 * Get a string representation of the object
 **/
	
	toString: function()
	{
		return this.label;
	}
});

/** section: alignment
 * class MatchedGroup
 *  
 * A group of matched items (all items deemed to be the same)
 *  
 **/

var MatchedGroup = Class.create({
	initialize: function(items)
	{
		if(!Object.isArray(items))
			items = [ items ];
			
		this.items = items;
		
		this.items.sort();
		
		var uid = "";
		var firstloop = true;
		this.items.each(function(item){
			if(!firstloop)
				uid += "_";
			uid += (item.id+"");
			firstloop = false;
		});
		
		this.id = uid;	// Give each group a unique id;
	},

/**
 * MatchedGroup#addItem() -> null
 * 
 * Add an item to the group
 **/
	
	addItem: function(item)
	{
		this.items.push(item);
		this.items.sort();
	},

/**
 * MatchedGroup#removeItem() -> null
 * 
 * Remove an item from the group
 **/
	
	removeItem: function(item)
	{
		// find the item to remove
		var i=0;
		var index = -1;
		
		this.items.each(function(c_item){
			if(item.id == c_item.id)
				index = i;
				
			i++;
		});
		
		if(index > -1)
			this.items.splice(index, 1);
	},
	
	toString: function()
	{
		if(this.items.length)
			return this.items[0].toString();
		else
			return "";
	}
});

var MatchedList = Class.create({
	all_groups: new Array(),
	all_groups_index: new Object(), // item.id => group.id
	all_groups_lookup: new Object(),// group.id => all_groups index
	all_groups_a2z: new Object(),	// group.id => A2Z index e.g. "a"
	
	az_indexes: null,
	
	initialize: function()
	{
		this.az_indexes = new Object();

		for(var i=65; i<=90; i++)
			this.az_indexes[String.fromCharCode(i).toLowerCase()] = new Array();

		this.az_indexes['#'] = new Array();
	},
	
	add: function(group)
	{
		this.all_groups.push(group);
		this.all_groups_lookup[group.id] = this.all_groups.length-1;
		
		group.items.each(function(item){
			this.all_groups_index[item.id] = group.id;
		}.bind(this));
		
		var chr = group.toString().substr(0,1).toLowerCase();
		
		this.az_indexes[chr].push(group);
		
		this.az_indexes[chr].sort();
		
		this.all_groups_a2z[group.id] = chr;
		
		return group;
	},
	
	remove: function(group)
	{
		var index = this.all_groups_lookup[group.id];
		
		// Remove from the master list
		this.all_groups.splice(index, 1);
		delete this.all_groups_lookup[group.id];
		
		// Remove from the A2Z indexes
		var i=0;
		var index=-1;
		
		var az = this.az_indexes[this.all_groups_a2z[group.id]];
		
		while(i < az.length)
		{
			if(az[i].id == group.id)
				index = i;
				
			i++;
		}
		
		if(index > -1)
		{
			az.splice(index, 1);
			delete this.all_groups_a2z[group.id];
		}
	},
	
	addItemToGroup: function(item, group)
	{
		this.all_groups_index[item.id] = group.id;
		group.addItem(item);
	},
	
	groupWithID: function(id)
	{
		var index = this.all_groups_lookup[id];
		
		if(index !== false)
			return this.all_groups[index];
		else
			return false;
	},
	
	groupForItem: function(item)
	{
		var id = this.all_groups_index[item.id];
		return this.groupWithID(id);
	},
	
	save: function()
	{
		//console.log(this.all_groups.toJSON());
		document.fire('loading:show', "Saving...");
		
		setTimeout( function() { document.fire('loading:hide') }, 1000);
		
		if(localStorage)
		{
			localStorage.setItem('matches', this.all_groups.toJSON());
		}
		else
			alert('saving not supported in this browser!');
	},
});

var DataStructure = new Class.create({
	all_data: null,						// Store all the raw data from the server
	all_data_index: new Object(),		// Provide a way to access raw data by ID
	
	az_indexes: null,					// A-Z Index for all raw data
	
	matched_data: new MatchedList(),			// List of matched objects
	matched_data_lookup: new Object(),	// List of ID's that have been matched
	
	hash_to_group: new Object(),		// hash of labels to groups (help with future processing)
	
	initialize: function(data)
	{
		this.all_data = data;
		
		// Iterate through all the data and build a hash to lookup objects based on ID's
		for(var i=0; i<this.all_data.length; i++)
			this.all_data_index[this.all_data[i].id] = i;
		
		this.indexData();
		
		if(localStorage)
		{
			var matches = localStorage.getItem('matches');
			
			if(matches)
			{
				matches = matches.evalJSON();
				matches.each(function(group){
					var items = new Array();
					
					group.items.each(function(item){
						items.push(new MatchedObject(item.label, item.id));
					});
					
					this.addMatches(items);
				}.bind(this));
			}
		}
		
		this.findMatches();
		
		document.observe('data:save', this.save.bindAsEventListener(this));
	},
	
	save: function(event)
	{
		this.matched_data.save();
	},
	
	itemWithID: function(id)
	{
		var index = this.all_data_index[id];
		
		if(!index)
			return false;
			
		return this.all_data[index];
	},
	
	findMatches: function()
	{
		// Try and do some basic guess work at alignments
		var hash_store = new Object();
		var matches = new Object();

		this.all_data.each(function(item){
			if(!this.matched_data_lookup[item.id])
			{
				// Create a hash of the label
				var hash = item.label.replace(/[^A-Za-z]/g, "").toLowerCase();

				var obj_to_store = new MatchedObject(item.label, item.id);
				
				// See if a group already exists from a previous session
				var g_id = this.hash_to_group[hash];
				
				if(!Object.isUndefined(g_id))
				{
					var group = this.matched_data.groupWithID(g_id);
					if(group)
						this.addItemToGroup(obj_to_store, group);
				}
				else
				{
					if(hash_store[hash])
					{
						// Hash is a match
						hash_store[hash].items.push(obj_to_store);
						matches[hash] = true;
					}
					else
					{
						// Hash does not match
						hash_store[hash] = new Object();
						hash_store[hash].items = new Array();

						hash_store[hash].items.push(obj_to_store);
					}
				}
			}
		}.bind(this));
		
		// Add the items we think are matches
		for(var hash in matches)
		{
			var items = hash_store[hash].items;
			this.addMatches(items);
		}
	},
	
	addMatches: function(items)
	{
		var group = new MatchedGroup(items);
		
		this.matched_data.add(group);
		
		group.items.each(function(item){
			// Mark the item as matched
			this.matched_data_lookup[item.id] = true;
			
			// Make a reference between the hash and the group id for future processing
			var hash = item.label.replace(/[^A-Za-z]/g, "").toLowerCase();
			this.hash_to_group[hash] = group.id
		}.bind(this));
	},
	
	addItemToGroup: function(item, group)
	{
		this.matched_data_lookup[item.id] = true;
		
		this.matched_data.addItemToGroup(new MatchedObject(item.label, item.id), group);
	},
	
	removeItemFromGroup: function(item)
	{
		var group = this.matched_data.groupForItem(item);
		group.removeItem(item);
		this.matched_data_lookup[item.id] = false;
		
		if(group.items.length == 0)
		{
			this.matched_data.remove(group);
		}
	},

	indexData: function()
	{
		// Create arrays for each A-Z
		this.az_indexes = new Object();

		for(var i=65; i<=90; i++)
			this.az_indexes[String.fromCharCode(i).toLowerCase()] = new Array();

		this.az_indexes['#'] = new Array();

		var i=0;
		this.all_data.each(function(item){
			var az = item.label.substr(0,1).toLowerCase();
			if(/[^a-z]/.test(az))
			{
				// Convert special chars to base letters (e.g. à => a)
				var cc = az.charCodeAt(0);
				if(cc >= 224 && cc <= 230)
					az = "a";
				else if(cc == 231)
					az = 'c';
				else if(cc >= 232 && cc <= 235)
					az = 'e';
				else if(cc >= 236 && cc <= 239)
					az = 'i';
				else if(cc == 241)
					az = 'n';
				else if(cc >= 242 && cc <= 246)
					az = 'o';
				else if(cc >= 249 && cc <= 252)
					az = 'u';
				else if(cc == 253)
					az = 'y';
				else if(cc == 254)
					az = 'p';
				else 
					az = '#';
			}

			this.az_indexes[az].push({id: item.id, label:item.label});
		}.bind(this));
	}
});